﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using Brjnk.Common;
using Brjnk.Common.Extensions;
using Brjnk.GpxExplorer.Data.ImportExport;
using Brjnk.GpxExplorer.UI;
using Brjnk.GpxExplorer.ViewModel;
using Brjnk.MVVM;

namespace Brjnk.GpxExplorer.Workspace
{
    public class DataManager
    {
        private DataModel data;

        private DiskConnectedWatcher watcher = new DiskConnectedWatcher();

        public PhotoManager PhotoManager { get; private set; }

        public ObservableCollection<ImportSource> Sources { get; set; }

        public DataManager(DataModel data, AreaManager area)
        {
            this.data = data;
            this.PhotoManager = new PhotoManager(data, area);

            Sources = new ObservableCollection<ImportSource>();

            watcher.DiskTypesFilter = (DiskTypes)int.MaxValue;
            watcher.DiskConnected += new DiskConnectedWatcher.DiskConnectedEventHandler(watcher_NewDiskConnected);
            watcher.DiskDisconnected += new DiskConnectedWatcher.DiskConnectedEventHandler(watcher_DiskDisconnected);

            CreateCommands();
            FindDevices();
            FindExternalApplications();
            allExternalApplications = new CompositeCollection() { new CollectionContainer() { Collection = ExternalApplications }, new CollectionContainer() { Collection = CustomExternalApplications } };
        }

        #region newDiskWatching

        private void FindDevices()
        {
            foreach (var device in System.IO.DriveInfo.GetDrives())
            {
                if (IsGpsDevice(device)) AddImportSource(device);
                if (Sources.Count >= 3) break;
            }
        }

        private void watcher_DiskDisconnected(System.IO.DriveInfo driveInfo)
        {
            System.Windows.Application.Current.Dispatcher.Invoke(new Action<DriveInfo>((d) => Disconnect(d)), driveInfo);
        }

        private void watcher_NewDiskConnected(System.IO.DriveInfo driveInfo)
        {
            System.Windows.Application.Current.Dispatcher.Invoke(new Action<DriveInfo>((d) => Connect(d)), driveInfo);
        }

        private void Connect(System.IO.DriveInfo driveInfo)
        {
            if (IsGpsDevice(driveInfo))
            {
                if (Sources.Count > 3) Sources.RemoveAt(0);
                AddImportSource(driveInfo);
            }
        }

        private void Disconnect(System.IO.DriveInfo driveInfo)
        {
            RemoveImportSource(driveInfo);
        }

        private void AddImportSource(System.IO.DriveInfo driveInfo)
        {
            string name = string.Format("[{0}] {1}", driveInfo.RootDirectory.FullName, driveInfo.VolumeLabel);
            Sources.Add(new ImportSource(driveInfo.Name, name, GetGpxPath(driveInfo)));
        }

        private void RemoveImportSource(DriveInfo driveInfo)
        {
            Sources.RemoveWhere(s => s.Id == driveInfo.Name);
        }

        private bool IsGpsDevice(System.IO.DriveInfo driveInfo)
        {
            var gpxPath = GetGpxPath(driveInfo);
            return System.IO.Directory.Exists(gpxPath);
        }

        private static string GetGpxPath(System.IO.DriveInfo driveInfo)
        {
            return System.IO.Path.Combine(driveInfo.RootDirectory.FullName, "Garmin\\GPX");
        }

        #endregion newDiskWatching

        private void FindExternalApplications()
        {
            externalApplications = new List<ExternalApplication>();

            externalApplications.Add(
                new PathExternalApplication("MapSource",
                    new GpxWriter(false),
                    App.Instance.FindResource("MapsourcePaths") as IEnumerable<string>));

            externalApplications.Add(
                 new PathExternalApplication("BaseCamp",
                    new GpxWriter(false),
                    App.Instance.FindResource("BaseCampPaths") as IEnumerable<string>));

            //Google Earth zatím není používán, protože neumí zpracovat GPX soubor předaný jako argument při spouštění
            //externalApplications.Add(
            //     new PathExternalApplication("Google Earth",
            //        new GpxWriter(false),
            //        App.Instance.FindResource("GoogleEarthPaths") as IEnumerable<string>));

            externalApplications.Add(
                 new PathExternalApplication("PSPad",
                    null,
                    App.Instance.FindResource("PSPadPaths") as IEnumerable<string>));
        }

        private void RemoveObject(object obj)
        {
            if (obj is WaypointVM)
                data.Waypoints.Remove((WaypointVM)obj);
            else if (obj is TrackVM)
            {
                data.Tracks.Remove((TrackVM)obj);
            }
            else if (obj is RouteVM)
            {
                data.Routes.Remove((RouteVM)obj);
            }
            else if (obj is IEnumerable)
            {
                foreach (var i in (obj as IEnumerable).Cast<object>().ToList())
                    RemoveObject(i);
            }
        }

        private void RemoveVisible(object arg)
        {
            var toRemove = data.GetAllObjects().Where(o => { return (o as IHideable).IsVisible; });
            RemoveObjects(toRemove);
        }

        private void RemoveHidden(object arg)
        {
            var toRemove = data.GetAllObjects().Where(o => { return (o as IHideable).IsVisible == false; });
            RemoveObjects(toRemove);
        }

        private void RemoveObjects(IEnumerable<DataItem> toRemove)
        {
            var result = UserMessages.Question(App.Instance.GetLocalizationString("$Loc$questionRemoveObjects"),
                 App.Instance.GetLocalizationString("$Loc$yes"),
                App.Instance.GetLocalizationString("$Loc$no"));
            if ((result ?? 1) == 0)
            {
                foreach (var item in toRemove.ToList())
                {
                    RemoveObject(item);
                }
            }
        }

        private GCBrowserWindow gcBrowserWindow;

        private void OpenGCBrowser(object arg)
        {
            if (gcBrowserWindow == null || gcBrowserWindow.IsVisible == false)
            {
                gcBrowserWindow = new GCBrowserWindow(data);
                gcBrowserWindow.Show();
            }
            gcBrowserWindow.Focus();
        }

        private void Import(string directory)
        {
            Microsoft.Win32.OpenFileDialog ofd = new Microsoft.Win32.OpenFileDialog();
            ofd.Multiselect = true;
            var reader = new GpxReader();
            ofd.Filter = reader.FileDialogFilter;
            if (System.IO.Directory.Exists(directory)) ofd.InitialDirectory = directory;
            if (ofd.ShowDialog() ?? false)
            {
                foreach (var fileName in ofd.FileNames)
                {
                    if (File.Exists(ofd.FileName))
                        Import(() => new FileStream(ofd.FileName, FileMode.Open), reader, ofd.FileName);
                }
            }
        }

        private void Import(Func<Stream> streamFactory, Data.ImportExport.GpxReader reader, string userDescription)
        {
            //Argumentem je StreamFactory, nikoliv FileStream, protože FileStream vytvořený na jiném vlákně nelze (pravděpodobně použít)
            BeginImportAsync(streamFactory, reader, userDescription);
        }

        private void BeginImportAsync(Func<Stream> streamFactory, Data.ImportExport.GpxReader reader, string userDescription)
        {
            var backgroundOperationState = new BackgroundOperationState("DataManager", "$Loc$workingImport", "empty", "empty", true);
            backgroundOperationState.SetInfo(string.Empty);
            backgroundOperationState.SetState(true);
            backgroundOperationState.SetLongInfo(userDescription);
            BackgroundOperationList.Instance.Add(backgroundOperationState);
            AsyncProcessing<DataReaderResult>.StartAsyncParallel(() => ImportAsync(streamFactory, reader), EndImportAsync, backgroundOperationState, ImportAsyncExceptionHandler);
        }

        private DataReaderResult ImportAsync(Func<Stream> streamFactory, Data.ImportExport.GpxReader reader)
        {
            using (var stream = streamFactory())
            {
                var result = reader.Load(stream);
                return result;
            }
        }

        private void ImportAsyncExceptionHandler(Exception e, object obj)
        {
            BackgroundOperationState state = (BackgroundOperationState)obj;
            BackgroundOperationList.Instance.Remove(state);
            System.Diagnostics.Debug.WriteLine("Unhandled exception during (async) parsing GPX file.");
            throw e;
        }

        private void EndImportAsync(DataReaderResult result, object obj)
        {
            BackgroundOperationState state = (BackgroundOperationState)obj;
            BackgroundOperationList.Instance.Remove(state);
            var window = new ImportExportWindow(result);
            if ((window.ShowDialog() ?? false))
            {
                this.data.Waypoints.AddRange(window.SelectedWaypoints);
                this.data.Routes.AddRange(window.SelectedRoutes);
                this.data.Tracks.AddRange(window.SelectedTracks);
            }
        }

        private void ImportSample(object arg)
        {
            var res = Application.GetResourceStream(new Uri(@"pack://application:,,,/GpxExplorer;Component/Resources/SampleGPX/" + arg.ToString()));
            Import(() => res.Stream, new GpxReader(), arg.ToString());
        }

        internal void ProcessCommandArgs()
        {
            var args = Environment.GetCommandLineArgs();
            for (int i = 1; i < args.Length; i++)
            {
                var arg = args[i];
                var fullPath = Path.GetFullPath(arg);
                var isValid = System.IO.File.Exists(fullPath) && string.Equals(Path.GetExtension(fullPath), ".gpx", StringComparison.InvariantCultureIgnoreCase);
                if (isValid)
                    this.Import(() => new System.IO.FileStream(fullPath, FileMode.Open), new GpxReader(), System.IO.Path.GetFileName(fullPath));
            }
        }

        private bool ignore;

        //TODO: Opravit. RibbonSplitButton spouští Command dvakrát. Proto je volána tato funkce, která vykoná pouze každé druhé volání
        private void ImportSampleFromRibbon(object arg)
        {
            ignore = !ignore;
            if (!ignore) ImportSample(arg);
        }

        private void ExportGPX()
        {
            ExportToFile(new GpxWriter(false));
        }

        private void ExportGC()
        {
            ExportToFile(new GpxWriter(true));
        }

        private void ExportToFile(IDataWriter writer)
        {
            var window = new ImportExportWindow
                (this.data.Waypoints, this.data.Routes, this.data.Tracks);
            if (!(window.ShowDialog() ?? false)) return;

            Microsoft.Win32.SaveFileDialog sfd = new Microsoft.Win32.SaveFileDialog();
            sfd.Filter = writer.FileDialogFilter;
            if (sfd.ShowDialog() ?? false)
            {
                BeginExportAsync(writer, () => new FileStream(sfd.FileName, FileMode.Create), new DataWriterArgs(window.SelectedWaypoints, window.SelectedRoutes, window.SelectedTracks), null, sfd.FileName);
            }
        }

        private void BeginExportAsync(IDataWriter writer, Func<Stream> streamFactory, DataWriterArgs dataWriterArgs, Action callback, string userDescription)
        {
            var backgroundOperationState = new BackgroundOperationState("DataManager", "$Loc$workingExport", "empty", "empty", true);
            backgroundOperationState.SetInfo(string.Empty);
            backgroundOperationState.SetState(true);
            backgroundOperationState.SetLongInfo(userDescription);
            BackgroundOperationList.Instance.Add(backgroundOperationState);
            AsyncProcessingNonGeneric.StartAsyncParallel(() => ExportAsync(streamFactory, writer, dataWriterArgs), (a) => EndExportAsync(a, callback), backgroundOperationState, ExportAsyncExceptionHandler);
        }

        private void EndExportAsync(object userObject, Action callback)
        {
            BackgroundOperationState state = (BackgroundOperationState)userObject;
            BackgroundOperationList.Instance.Remove(state);
            if (callback != null) callback();
        }

        private void ExportAsyncExceptionHandler(Exception e, object obj)
        {
            BackgroundOperationState state = (BackgroundOperationState)obj;
            BackgroundOperationList.Instance.Remove(state);
            System.Diagnostics.Debug.WriteLine("Unhandled exception during (async) export.");
            throw e;
        }

        private void ExportAsync(Func<Stream> streamFactory, IDataWriter writer, DataWriterArgs data)
        {
            using (var fs = streamFactory())
            {
                writer.Write(data.Waypoints, data.Routes, data.Tracks, fs);
            }
        }

        #region ExternalApplications

        private void ManageExternalApplications()
        {
            var window = new ManageExternalApplicationsWindow(this);
            window.ShowDialog();
        }

        private List<ExternalApplication> externalApplications;

        private CompositeCollection allExternalApplications;

        public ObservableCollection<CustomExternalApplication> CustomExternalApplications { get { return App.Instance.Settings.CustomExternalApplications; } }

        public IEnumerable<ExternalApplication> ExternalApplications { get { return externalApplications; } }

        public CompositeCollection AllExternalApplications { get { return allExternalApplications; } }

        private void OpenExternalApplication(object arg)
        {
            var window = new ImportExportWindow(this.data.Waypoints,
                this.data.Routes,
                this.data.Tracks);

            if (!(window.ShowDialog() ?? false)) return;

            var app = arg as ExternalApplication;
            var writer = app.Writer;
            //Pokud není definován writer, musí být zadán uživatelem
            if (writer == null)
            {
                var result = UserMessages.Question(
                    App.Instance.GetLocalizationString("$Loc$questionUseGcGpx"),
                    App.Instance.GetLocalizationString("$Loc$answerUseDefaultGpx"),
                    App.Instance.GetLocalizationString("$Loc$answerUseGcGpx"),
                    App.Instance.GetLocalizationString("$Loc$cancel"));
                if (result.HasValue == false || result.Value == 2) return;
                writer = new GpxWriter(result.Value == 1);
            }
            var fileName = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "GpxExplorerTMP" + Guid.NewGuid().ToString() + writer.Extension);

            var args = new DataWriterArgs(window.SelectedWaypoints, window.SelectedRoutes, window.SelectedTracks);
            BeginExportAsync(writer, () => new System.IO.FileStream(fileName, FileMode.Create), args, () => Start(app.Path, fileName), app.Name);
        }

        private void Start(string app, string fileName)
        {
            try
            {
                System.Diagnostics.Process.Start(app, "\"" + fileName + "\"");
            }
            catch (Exception ex)
            {
                MessageBox.Show("Process start error: " + app + "\n" + ex.Message, "Start process failed");
            }
        }

        private void AddExternalApplication()
        {
            CustomExternalApplications.Add(new CustomExternalApplication() { NameSerializable = "ExternalApplication" });
        }

        private void RemoveExternalApplication(CustomExternalApplication ep)
        {
            CustomExternalApplications.RemoveWhere(i => i == ep);
        }

        #endregion ExternalApplications

        #region Commands

        private void CreateCommands()
        {
            importCommand = new GenericCommand(o => Import(o as string));
            exportGCCommand = new GenericCommand(o => ExportGC());
            exportGPXCommand = new GenericCommand(o => ExportGPX());
            openExternalApplicationCommand = new GenericCommand(OpenExternalApplication);
            manageExternalApplicationsCommand = new GenericCommand(o => ManageExternalApplications());
            addExternalApplicationCommand = new GenericCommand(o => AddExternalApplication());
            removeExternalApplicationCommand = new GenericCommand(o => RemoveExternalApplication(o as CustomExternalApplication));
            removeObjectCommand = new GenericCommand(RemoveObject);
            RemoveVisibleCommand = new GenericCommand(RemoveVisible);
            RemoveHiddenCommand = new GenericCommand(RemoveHidden); ;
            ImportSampleCommand = new GenericCommand(ImportSampleFromRibbon);
            OpenGCBrowserCommand = new GenericCommand(OpenGCBrowser);
        }

        private GenericCommand importCommand;

        public GenericCommand ImportCommand { get { return importCommand; } }

        private GenericCommand exportGPXCommand;

        public GenericCommand ExportGPXCommand { get { return exportGPXCommand; } }

        private GenericCommand exportGCCommand;

        public GenericCommand ExportGCCommand { get { return exportGCCommand; } }

        private GenericCommand openExternalApplicationCommand;

        public GenericCommand OpenExternalApplicationCommand { get { return openExternalApplicationCommand; } }

        private GenericCommand manageExternalApplicationsCommand;

        public GenericCommand ManageExternalApplicationsCommand { get { return manageExternalApplicationsCommand; } }

        private GenericCommand addExternalApplicationCommand;

        public GenericCommand AddExternalApplicationCommand { get { return addExternalApplicationCommand; } }

        private GenericCommand removeExternalApplicationCommand;

        public GenericCommand RemoveExternalApplicationCommand { get { return removeExternalApplicationCommand; } }

        private GenericCommand removeObjectCommand;

        public GenericCommand RemoveObjectCommand { get { return removeObjectCommand; } }

        public GenericCommand RemoveVisibleCommand { get; private set; }

        public GenericCommand RemoveHiddenCommand { get; private set; }

        public GenericCommand ImportSampleCommand { get; private set; }

        public GenericCommand OpenGCBrowserCommand { get; private set; }

        #endregion Commands
    }

    public struct ImportSource
    {
        public ImportSource(string id, string name, string root)
            : this()
        {
            Id = id;
            Name = name;
            DirectoryPath = root;
        }

        public string Id { get; private set; }

        public string Name { get; private set; }

        public string DirectoryPath { get; private set; }
    }
}